// 
//  Copyright © 2008, 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 

using System;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.Generic;

using Galaxium.Protocol.Xmpp.Library.Utility;

namespace Galaxium.Protocol.Xmpp.Library.Xml
{
	public class Element: IEnumerable<Element>
	{
		private string _text;
		private Element _first_child;
		private Element _last_child;
		
		private Dictionary<string, string> _prefix_namespaces;
		
		#region properties
		
		public string Prefix { get; private set; }
		public string Name { get; private set; }
		
		public Dictionary<string, string> Attributes { get; private set; }
		
		public Element Parent   { get; private set; }
		public Element Previous { get; private set; }
		public Element Next     { get; private set; }

		
		public bool IsEmpty {
			get { return _text == null && _first_child == null; }
		}
		
		public bool IsTextOnly {
			get {
				return _first_child != null &&
					_first_child.IsAnonymous &&
						_first_child == _last_child;
			}
		}

		public bool IsAnonymous {
			get { return Name == null; }
		}

		public bool HasChildren {
			get { return _first_child != null; }
		}
		
		public string Namespace {
			get { return GetAttribute ("xmlns"); }
			set { SetAttribute ("xmlns", value); }
		}
		
		public string Text {
			get { return GetText (); }
			set { SetText (value); }
		}
		
		public string this [string index] {
			get { return GetAttribute (index); }
			set { SetAttribute (index, value); }
		}

		#endregion
		#region constructors

		public Element ()
			:this (null, null, (Dictionary<string,string>) null)
		{
		}
		
		public Element (string name)
			:this (null, name, (Dictionary<string,string>) null)
		{
		}
		
		public Element (string name, string xmlns)
			:this (null, name, xmlns)
		{
		}
		
		public Element (string prefix, string name, string xmlns)
			:this (prefix, name, (Dictionary<string,string>) null)
		{
			Namespace = xmlns;
		}
		
		public Element (string prefix, string name, Dictionary<string, string> attribs)
		{
			if (name == null && attribs != null)
				throw new ArgumentNullException ("name");
			
			Prefix = prefix;
			Name = name;
			
			if (Name != null) {
				Attributes = attribs ?? new Dictionary<string, string> ();
				_prefix_namespaces = new Dictionary<string, string> ();
			}
		}
		
		public Element (string prefix, string name, string xmlns, params string[] attribs)
		{
			if (name == null)
				throw new ArgumentNullException ("name");
			
			Prefix = prefix;
			Name = name;
			Attributes = new Dictionary<string, string> ();
			_prefix_namespaces = new Dictionary<string, string> (); 
			if (xmlns != null)
				Attributes ["xmlns"] = xmlns;
			for (var i = 0; i < attribs.Length / 2; i ++)
				if (attribs [i + 1] != null)
					Attributes [attribs [i]] = attribs [i + 1];
		}
		
		protected Element (Element elm)
			:this (elm.Name)
		{
			Envelope (elm);
		}
		
		#endregion
		
		public Element Clone ()
		{
			var clone = (Element) Activator.CreateInstance (this.GetType ());
			if (IsAnonymous) {
				clone._text = _text;
			}
			else {
				clone.Name = Name;
				clone.Attributes = new Dictionary<string, string> ();
				foreach (var attr in Attributes)
					clone [attr.Key] = attr.Value;
				foreach (var child in this)
					clone.AppendChild (child.Clone ());
			}
			return clone;
		}

		protected void Envelope (Element elm)
		{
			Attributes = elm.Attributes;
			elm.Attributes = null;
			_prefix_namespaces = elm._prefix_namespaces;			
			_first_child = elm._first_child;
			_last_child = elm._last_child;
			elm._prefix_namespaces = null;
			elm._first_child = null;
			elm._last_child = null;
			elm.ReplaceWith (this);
		}

		#region children

		public void AppendChild (Element child)
		{
			if (IsAnonymous)
				throw new InvalidOperationException ("You can't append children to an anonymous node.");
			
			if (child == null)
				throw new ArgumentNullException ("child");
			
			if (child.Parent != null)
				throw new ArgumentException ("Element already has a parent.");
			
			if (IsEmpty) {
				child.Parent = this;
				_first_child = child;
				_last_child = child;
			}
			else if (_last_child.IsAnonymous && child.IsAnonymous){
				_last_child.Text += child.Text;
			}
			else {
				child.Parent = this;
				child.Previous = _last_child;
				_last_child.Next = child;
				_last_child = child;
			}
		}

		public void Clear ()
		{
			if (IsAnonymous) {
				_text = null;
			}
			else {
				foreach (var child in this) {
					if (child.Previous != null)
						child.Previous.Next = null;
					child.Previous = null;
					child.Parent = null;
				}
				_first_child = null;
				_last_child = null;
			}
		}

		// doesn't return anonymous children
		public Element FirstChild (string name, string xmlns)
		{
			foreach (var child in this) {
				if (!child.IsAnonymous &&
				    (name == null || child.Name == name) &&
				    (xmlns == null || child.Namespace == xmlns))
					return child;
			}
			return null;
		}

		#endregion
		#region attribs
		
		public Element SetAttribute (string name, string val)
		{
			if (IsAnonymous)
				throw new InvalidOperationException ("Anonymous element can't have any attributes.");
			if (val == null) Attributes.Remove (name);
			else Attributes [name] = val;
			return this;
		}
		
		public string GetAttribute (string name)
		{
			if (IsAnonymous)
				throw new InvalidOperationException ("Anonymous element can't have any attributes.");
			
			string val;
			return Attributes.TryGetValue (name, out val) ? val : null;
		}

		#endregion
		
		public string GetText ()
		{
			if (IsAnonymous || IsEmpty)
				return _text ?? String.Empty;

			if (IsTextOnly)
				return _first_child.Text;

			throw new InvalidOperationException ("Element contains children.");
		}
		
		public void Unparent ()
		{
			if (Parent == null) return;
			if (Previous == null) Parent._first_child = Next;
			else Previous.Next = Next;
			if (Next == null) Parent._last_child = Previous;
			else Next.Previous = Previous;
			Parent = Previous = Next = null;
		}
		
		public void ReplaceWith (Element elm)
		{
			if (elm.Parent != null)
				throw new InvalidOperationException ("Element already has a parent.");
			if (Parent == null) return;
			if (Previous == null) Parent._first_child = elm;
			else Previous.Next = elm;
			if (Next == null) Parent._last_child = elm;
			else Next.Previous = elm;
			elm.Parent = Parent;
			Parent = Previous = Next = null;
		}
		
		public Element SetText (string text)
		{
			if (HasChildren && !IsTextOnly)
				throw new InvalidOperationException ("Element contains children.");

			if (IsAnonymous) {
				_text = text;
				if (String.IsNullOrEmpty (text)) Unparent ();
			}
			else if (IsEmpty) {
				if (!String.IsNullOrEmpty (text))
					AppendText (text);
			}
			else {
				_first_child.SetText (text);
			}
			return this;
		}
		
		public void AppendText (string text)
		{
			if (text == null)
				throw new ArgumentNullException ("text");
			
			if (IsAnonymous)
				_text += text;
			else if (_last_child == null || !_last_child.IsAnonymous)
				AppendChild (new Element ().SetText (text));
			else
				_last_child.AppendText (text);
		}

		public string GetTextChild (string child_name, string child_namespace)
		{
			var elm = FirstChild (child_name, child_namespace);
			return (elm == null) ? null : elm.Text;
		}
		
		public void AppendTextChild (string child_name, string child_namespace, string text)
		{
			if (child_name == null)
				throw new ArgumentNullException ("child_name");
			if (text == null)
				throw new ArgumentNullException ("text");
			AppendChild (new Element (child_name, child_namespace).SetText (text));
		}
		
		IEnumerator IEnumerable.GetEnumerator ()
		{
			return GetEnumerator ();
		}

		public IEnumerator<Element> GetEnumerator ()
		{
			var child = _first_child;
			while (child != null) {
				yield return child;
				child = child.Next;
			}
		}
		
		public IEnumerable<Element> EachChild (string name)
		{
			return new ChildEnumerable (this, name);
		}
		
		private class ChildEnumerable: IEnumerable<Element>
		{
			private Element _element;
			private string _name;
			
			public ChildEnumerable (Element element, string name)
			{
				_element = element;
				_name = name;
			}
			
			public IEnumerator<Element> GetEnumerator ()
			{
				foreach (var child in _element)
					if (child.Name == _name) yield return child;
			}
			
			IEnumerator IEnumerable.GetEnumerator ()
			{
				return GetEnumerator ();
			}
		}
		
		public override string ToString ()
		{
			throw new NotImplementedException ();
		}
	}
}